The following are tables of the Blue and Red groups and the model decisions/probabilities. We will use them to calculate the fairness coefficients.
| Blue | Will use XAI (1) | Will not use XAI (0) | Total |
|---|---|---|---|
| Enrolled in training (1) | 60 | 5 | 65 |
| Not enrolled in training (0) | 20 | 15 | 35 |
| Total | 80 | 20 | 100 |
| Red | Will use XAI (1) | Will not use XAI (0) | Total |
|---|---|---|---|
| Enrolled in training (1) | 25% | 25% | 50% |
| Not enrolled in training (0) | 25% | 25% | 50% |
| Total | 50% | 50% | 100% |
Blue: $ P(\hat{Y}=1 | color=Blue) = 65 / (65+35) = 0.65$
Red: $ P(\hat{Y}=1 | color=Red) = 50\% / (50\%+50\%) = 0.5$
Red is slightly underprivileged. The Coefficient here is $\frac{0.65}{0.5} = 130\% > 125\%$
Blue: $ P(\hat{Y}=1 | Y = 1, color=Blue) = 60 / (60+20) = 0.75$
Red: $ P(\hat{Y}=1 | Y = 1, color=Red) = 25\% / (25\%+25\%) = 0.5$
Red is underprivileged. The Coefficient here is $\frac{0.75}{0.5} = 150\% > 125\%$
Blue: $ P(Y = 1 | \hat{Y}=1, color=Blue) = 60 / (60+5) \approx 0.92$
Red: $ P(Y = 1| \hat{Y}=1, color=Red) = 25\% / (25\%+25\%) = 0.5$
Red is very underprivileged. The Coefficient here is $\frac{0.92}{0.5} \approx 184\% > 125\%$
Assuming it is very hard to create a predictive model for Red and we assign the enrollments there randomly, we can still change the percentage of the group getting the enrollment. Noting that in the case of random enrollment, since then $\hat{Y} \perp Y$, by changing the rate of assingment in the red group, we directly change the demographic parity coeeficient and the equal opportunity coefficient. As such, increasing the rate of assignment to 65% instantly changes:
Demographic parity: $P(\hat{Y}=1 | color=Red) = 65\% / (65\%+35\%) = 0.65$
Equal opportunity: $P(\hat{Y}=1 | Y = 1, color=Red) = P(\hat{Y}=1 | color=Red) = 65\% / (65\%+35\%) = 0.65$
In both cases the coefficient improves: Demographic parity: $\frac{0.65}{0.65} = 100\% \leq 125\%$
Equal opportunity: $\frac{0.75}{0.65} \approx 115\% \leq 125\%$
Since $\hat{Y} \perp Y$, the Predictive rate parity does not change at all and remains at the same levels.
This way, we improved on 2 of the 3 equality coefficients, with the third remaining constant.
We decide to use the Adult income dataset (https://www.kaggle.com/datasets/wenruliu/adult-income-dataset/). The purpose of the dataset is to train a model in predicting whether a person has over 50K income. After quickly cleaning the data (conversion of categorical columns into one-hot encoded), we divided the data randomly into 90% train and 10% test subsets. We proceeded to train predictive models and check their accuracy as well as their bias against females.
Models we trained were:
| Train accuracy | Test accuracy | TPR (female) | ACC (female) | PPV (female) | FPR (female) | STP (female) | |
|---|---|---|---|---|---|---|---|
| Logistic regression | 0.796 | 0.801 | 0.992032 | 1.188579 | 0.663087 | 0.833333 | 0.53 |
| Random Forest | 0.863 | 0.858 | 0.729433 | 1.125 | 1.012626 | 0.183333 | 0.254902 |
| Logistic regression (bonus 0.1)* | 0.796 | 0.800 | 1.035857 | 1.184595 | 0.62953 | 0.944444 | 0.58 |
| Logistic regression (bonus 0.2)* | 0.795 | 0.798 | 1.059761 | 1.177955 | 0.58255 | 1.138889 | 0.64 |
| Logistic regression (bonus 0.3)* | 0.795 | 0.797 | 1.059761 | 1.176627 | 0.571812 | 1.166667 | 0.65 |
| Logistic regression (bonus 0.4)* | 0.793 | 0.796 | 1.059761 | 1.169987 | 0.536913 | 1.305556 | 0.7 |
| Logistic regression (bonus 0.5)* | 0.790 | 0.792 | 1.083665 | 1.155378 | 0.467114 | 1.638889 | 0.82 |
Note: *Bonus points assigned to female when predicting high income; Bolded highest accuracies; Bolded fairness ratios within the 4/5 rule
import copy
import numpy as np
import pandas as pd
df = pd.read_csv('adult.csv')
df.head()
| age | workclass | fnlwgt | education | educational-num | marital-status | occupation | relationship | race | gender | capital-gain | capital-loss | hours-per-week | native-country | income | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 25 | Private | 226802 | 11th | 7 | Never-married | Machine-op-inspct | Own-child | Black | Male | 0 | 0 | 40 | United-States | <=50K |
| 1 | 38 | Private | 89814 | HS-grad | 9 | Married-civ-spouse | Farming-fishing | Husband | White | Male | 0 | 0 | 50 | United-States | <=50K |
| 2 | 28 | Local-gov | 336951 | Assoc-acdm | 12 | Married-civ-spouse | Protective-serv | Husband | White | Male | 0 | 0 | 40 | United-States | >50K |
| 3 | 44 | Private | 160323 | Some-college | 10 | Married-civ-spouse | Machine-op-inspct | Husband | Black | Male | 7688 | 0 | 40 | United-States | >50K |
| 4 | 18 | ? | 103497 | Some-college | 10 | Never-married | ? | Own-child | White | Female | 0 | 0 | 30 | United-States | <=50K |
for col in df:
print(f"Column {col}: {df[col].unique()}")
df.dtypes
age int64 workclass object fnlwgt int64 education object educational-num int64 marital-status object occupation object relationship object race object gender object capital-gain int64 capital-loss int64 hours-per-week int64 native-country object income object dtype: object
df2 = pd.get_dummies(df)
df2.head()
del df2[df2.columns[-2]]
df2.head()
X = df2.loc[:, df2.columns != 'income_>50K']
y = df2.loc[:, df2.columns == 'income_>50K']
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=2)
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
regr = LogisticRegression(random_state=2).fit(X_train, y_train)
print(f"Accuracy: Train: {accuracy_score(y_train, regr.predict(X_train))} Test: {accuracy_score(y_test, regr.predict(X_test))}")
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\utils\validation.py:1111: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel(). y = column_or_1d(y, warn=True)
Accuracy: Train: 0.7975066542302705 Test: 0.8010235414534289
import dalex as dx
explainer = dx.Explainer(regr, X_test, y_test, label='Logistic Regression', verbose=False)
explainer.model_performance()
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\base.py:450: UserWarning: X does not have valid feature names, but LogisticRegression was fitted with feature names
| recall | precision | f1 | accuracy | auc | |
|---|---|---|---|---|---|
| Logistic Regression | 0.250664 | 0.691932 | 0.36801 | 0.801024 | 0.577077 |
protected_variable = X_test.gender_Male.apply(lambda x: "male" if x else "female")
privileged_group = "male"
fobject = explainer.model_fairness(
protected=protected_variable,
privileged=privileged_group
)
fobject.fairness_check()
Bias detected in 2 metrics: PPV, STP
Conclusion: your model is not fair because 2 or more criteria exceeded acceptable limits set by epsilon.
Ratios of metrics, based on 'male'. Parameter 'epsilon' was set to 0.8 and therefore metrics should be within (0.8, 1.25)
TPR ACC PPV FPR STP
female 0.992032 1.188579 0.663087 0.833333 0.53
fobject.plot()
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(random_state=2, max_depth=10).fit(X_train, y_train)
print(f"Accuracy: Train: {accuracy_score(y_train, rf.predict(X_train))} Test: {accuracy_score(y_test, rf.predict(X_test))}")
C:\Users\Antek\AppData\Local\Temp\ipykernel_11860\4048729203.py:2: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples,), for example using ravel().
Accuracy: Train: 0.8636167163364197 Test: 0.858546571136131
explainer2 = dx.Explainer(rf, X_test, y_test, label='Random Forest', verbose=False)
explainer2.model_performance()
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\base.py:450: UserWarning: X does not have valid feature names, but RandomForestClassifier was fitted with feature names
| recall | precision | f1 | accuracy | auc | |
|---|---|---|---|---|---|
| Random Forest | 0.524358 | 0.793566 | 0.631467 | 0.858547 | 0.908062 |
protected_variable = X_test.gender_Male.apply(lambda x: "male" if x else "female")
privileged_group = "male"
fobject2 = explainer2.model_fairness(
protected=protected_variable,
privileged=privileged_group
)
fobject2.fairness_check()
Bias detected in 3 metrics: TPR, FPR, STP
Conclusion: your model is not fair because 2 or more criteria exceeded acceptable limits set by epsilon.
Ratios of metrics, based on 'male'. Parameter 'epsilon' was set to 0.8 and therefore metrics should be within (0.8, 1.25)
TPR ACC PPV FPR STP
female 0.729433 1.125 1.012626 0.183333 0.254902
fobject2.plot()
class regr_with_bonus(LogisticRegression):
def __init__(self, attribute_to_bonus='gender_Female', bonus_amount=0, **kwargs):
LogisticRegression.__init__(self, **kwargs)
self.attribute_to_bonus = attribute_to_bonus
self.bonus_amount = bonus_amount
def predict(self, X):
a = (((np.dot(X, self.coef_.T)+self.intercept_+(self.bonus_amount*X[[self.attribute_to_bonus]]))>0)*1).to_numpy()
return a.ravel()
regr3 = regr_with_bonus(random_state=2, attribute_to_bonus='gender_Female', bonus_amount=0.1).fit(X_train, y_train)
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\utils\validation.py:1111: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
print(f"Accuracy: Train: {accuracy_score(y_train, regr3.predict(X_train))} Test: {accuracy_score(y_test, regr3.predict(X_test))}")
Accuracy: Train: 0.7965284255067452 Test: 0.8
def predictFunction(model, data):
return model.predict(data)
from copy import deepcopy
bonus_models = []
for female_bonus in [0.1, 0.2, 0.3, 0.4, 0.5]:
print("Bonus: ",female_bonus)
regr3 = regr_with_bonus(random_state=2, attribute_to_bonus='gender_Female', bonus_amount=female_bonus).fit(X_train, y_train)
print(f"Accuracy: Train: {accuracy_score(y_train, regr3.predict(X_train))} Test: {accuracy_score(y_test, regr3.predict(X_test))}")
explainer4 = dx.Explainer(regr3, X_test, y_test, predict_function=predictFunction, verbose=False, label='LogisticRegression with female bonus '+str(female_bonus))
fobject4 = explainer4.model_fairness(
protected=protected_variable,
privileged=privileged_group)
fobject4.fairness_check()
bonus_models.append(deepcopy(fobject4))
Bonus: 0.1
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\utils\validation.py:1111: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
Accuracy: Train: 0.7965284255067452 Test: 0.8
Bias detected in 2 metrics: PPV, STP
Conclusion: your model is not fair because 2 or more criteria exceeded acceptable limits set by epsilon.
Ratios of metrics, based on 'male'. Parameter 'epsilon' was set to 0.8 and therefore metrics should be within (0.8, 1.25)
TPR ACC PPV FPR STP
female 1.035857 1.184595 0.62953 0.944444 0.58
Bonus: 0.2
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\utils\validation.py:1111: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
Accuracy: Train: 0.795732192824806 Test: 0.7983623336745138
Bias detected in 2 metrics: PPV, STP
Conclusion: your model is not fair because 2 or more criteria exceeded acceptable limits set by epsilon.
Ratios of metrics, based on 'male'. Parameter 'epsilon' was set to 0.8 and therefore metrics should be within (0.8, 1.25)
TPR ACC PPV FPR STP
female 1.059761 1.177955 0.58255 1.138889 0.64
Bonus: 0.3
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\utils\validation.py:1111: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
Accuracy: Train: 0.7951634551948495 Test: 0.7979529170931423
Bias detected in 2 metrics: PPV, STP
Conclusion: your model is not fair because 2 or more criteria exceeded acceptable limits set by epsilon.
Ratios of metrics, based on 'male'. Parameter 'epsilon' was set to 0.8 and therefore metrics should be within (0.8, 1.25)
TPR ACC PPV FPR STP
female 1.059761 1.176627 0.571812 1.166667 0.65
Bonus: 0.4
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\utils\validation.py:1111: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
Accuracy: Train: 0.7936619878517642 Test: 0.7965199590583418
Bias detected in 3 metrics: PPV, FPR, STP
Conclusion: your model is not fair because 2 or more criteria exceeded acceptable limits set by epsilon.
Ratios of metrics, based on 'male'. Parameter 'epsilon' was set to 0.8 and therefore metrics should be within (0.8, 1.25)
TPR ACC PPV FPR STP
female 1.059761 1.169987 0.536913 1.305556 0.7
Bonus: 0.5
C:\Users\Antek\anaconda3\lib\site-packages\sklearn\utils\validation.py:1111: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
Accuracy: Train: 0.790431558113611 Test: 0.792835209825998
Bias detected in 2 metrics: PPV, FPR
Conclusion: your model is not fair because 2 or more criteria exceeded acceptable limits set by epsilon.
Ratios of metrics, based on 'male'. Parameter 'epsilon' was set to 0.8 and therefore metrics should be within (0.8, 1.25)
TPR ACC PPV FPR STP
female 1.083665 1.155378 0.467114 1.638889 0.82
fobject.plot(bonus_models+[fobject2], show=False).\
update_layout(autosize=False, width=950, height=450, legend=dict(yanchor="top", y=1.29, xanchor="right", x=0.99))